package org.lemon.transport.context.adaptor;

import com.google.gson.*;
import com.google.protobuf.InvalidProtocolBufferException;
import org.lemon.transport.context.data.DataConstants;
import org.lemon.transport.context.data.kv.*;
import org.server.transport.gen.SessionProto;
import org.server.transport.gen.TransportProto;
import org.server.transport.gen.TransportProto.*;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;

public class JsonConverter {

    private static final Gson GSON = new Gson();
    private static final JsonParser JSON_PARSER = new JsonParser();
    private static final String CAN_T_PARSE_VALUE = "Can't parse value: ";
    private static final String DEVICE_PROPERTY = "device";

    public static PostTelemetryMsg convertToTelemetryProto(long ts, JsonElement jsonElement) {

        PostTelemetryMsg.Builder builder = PostTelemetryMsg.newBuilder();
        convertToTelemetry(jsonElement, ts, builder);
        return builder.build();
    }

    public static PostTelemetryMsg convertToTelemetryProto(JsonElement jsonElement) {

        PostTelemetryMsg.Builder builder = PostTelemetryMsg.newBuilder();
        convertToTelemetry(jsonElement, System.currentTimeMillis(), builder);
        return builder.build();
    }

    public static PostAttributeMsg convertToAttributesProto(JsonElement jsonObject) throws JsonSyntaxException {
        if (jsonObject.isJsonObject()) {
            PostAttributeMsg.Builder result = PostAttributeMsg.newBuilder();
            List<KeyValueProto> keyValueList = parseValues(jsonObject.getAsJsonObject());
            result.addAllKv(keyValueList);
            return result.build();
        } else {
            throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonObject);
        }
    }

    public static ClaimDeviceMsg convertToClaimDeviceProto(Long deviceId, String json) {
        long durationMs = 0L;
        if (json != null && !json.isEmpty()) {
            return convertToClaimDeviceProto(deviceId, JSON_PARSER.parse(json));
        }
        return buildClaimDeviceMsg(deviceId, DataConstants.DEFAULT_SECRET_KEY, durationMs);
    }

    public static ClaimDeviceMsg convertToClaimDeviceProto(Long deviceId, JsonElement jsonElement) {

        String secretKey = DataConstants.DEFAULT_SECRET_KEY;
        long durationMs = 0L;
        if (jsonElement.isJsonObject()) {
            JsonObject jo = jsonElement.getAsJsonObject();
            if (jo.has(DataConstants.SECRET_KEY_FIELD_NAME)) {
                secretKey = jo.get(DataConstants.SECRET_KEY_FIELD_NAME).getAsString();
            }
            if (jo.has(DataConstants.DURATION_MS_FIELD_NAME)) {
                durationMs = jo.get(DataConstants.DURATION_MS_FIELD_NAME).getAsLong();
            }
        } else {
            throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonElement);
        }
        return buildClaimDeviceMsg(deviceId, secretKey, durationMs);
    }

    public static ProvisionDeviceRequestMsg convertToProvisionRequestMsg(JsonElement element) {
        if (element.isJsonObject()) {
            ProvisionDeviceRequestMsg.Builder builder = ProvisionDeviceRequestMsg.newBuilder();
            JsonObject jsonObject = element.getAsJsonObject();
            builder.setDeviceName(getStrValue(jsonObject, DataConstants.DEVICE_NAME, true));
            CredentialsType credentialsType = jsonObject.get(DataConstants.CREDENTIALS_TYPE) != null ?
                    CredentialsType.valueOf(getStrValue(jsonObject, DataConstants.CREDENTIALS_TYPE, false)) : CredentialsType.ACCESS_TOKEN;
            builder.setCredentialsType(credentialsType);
            CredentialsDataProto.Builder creBuilder = CredentialsDataProto.newBuilder();
            switch (credentialsType) {
                case ACCESS_TOKEN:
                    creBuilder.setValidateDeviceTokenRequestMsg(SessionProto.ValidateDeviceTokenRequestMsg.newBuilder()
                            .setToken(getStrValue(jsonObject, DataConstants.TOKEN, false))
                            .build());
                    break;
                case MQTT_BASIC:
                    creBuilder.setValidateBasicMqttCredRequestMsg(SessionProto.ValidateBasicMqttCredRequestMsg.newBuilder().setClientId(getStrValue(jsonObject, DataConstants.CLIENT_ID, false))
                            .setUserName(getStrValue(jsonObject, DataConstants.USERNAME, false))
                            .setPassword(getStrValue(jsonObject, DataConstants.PASSWORD, false))
                            .build());
                    break;
                case X509_CERTIFICATE:
                    creBuilder.setValidateDeviceX509CertRequestMsg(SessionProto.ValidateDeviceX509CertRequestMsg.newBuilder()
                            .setHash(getStrValue(jsonObject, DataConstants.HASH, false))
                            .build());
                    break;
            }
            builder.setCredentialsDataProto(creBuilder.build());
            builder.setProvisionDeviceCredentialsMsg(ProvisionDeviceCredentialsMsg.newBuilder()
                    .setProvisionDeviceKey(getStrValue(jsonObject, DataConstants.PROVISION_KEY, false))
                    .setProvisionDeviceSecret(getStrValue(jsonObject, DataConstants.PROVISION_SECRET, false))
                    .build());
            return builder.build();
        } else {
            throw new JsonSyntaxException(CAN_T_PARSE_VALUE + element);
        }
    }

    public static JsonObject toJson(ProvisionDeviceResponseMsg payload) {
        return toJson(payload, false, 0);
    }

    public static JsonObject toJson(GetAttributeResponseMsg payload) {

        JsonObject result = new JsonObject();
        if (payload.getClientAttributeListCount() > 0) {
            JsonObject attrObject = new JsonObject();
            payload.getClientAttributeListList().forEach(addToObjectFromProto(attrObject));
            result.add("client", attrObject);
        }
        if (payload.getSharedAttributeListCount() > 0) {
            JsonObject attrObject = new JsonObject();
            payload.getSharedAttributeListList().forEach(addToObjectFromProto(attrObject));
            result.add("shared", attrObject);
        }
        return result;
    }

    public static JsonObject toJson(AttributeUpdateNotificationMsg payload) {

        JsonObject result = new JsonObject();
        if (payload.getSharedUpdatedCount() > 0) {
            payload.getSharedUpdatedList().forEach(addToObjectFromProto(result));
        }
        if (payload.getSharedDeletedCount() > 0) {
            JsonArray attrObject = new JsonArray();
            payload.getSharedDeletedList().forEach(attrObject::add);
            result.add("deleted", attrObject);
        }
        return result;
    }

    private static Consumer<TsKeyValueProto> addToObjectFromProto(JsonObject result) {
        return de -> {
            switch (de.getKv().getType()) {
                case BOOLEAN_V:
                    result.add(de.getKv().getKey(), new JsonPrimitive(de.getKv().getBoolV()));
                    break;
                case DOUBLE_V:
                    result.add(de.getKv().getKey(), new JsonPrimitive(de.getKv().getDoubleV()));
                    break;
                case LONG_V:
                    result.add(de.getKv().getKey(), new JsonPrimitive(de.getKv().getLongV()));
                    break;
                case STRING_V:
                    result.add(de.getKv().getKey(), new JsonPrimitive(de.getKv().getStringV()));
                    break;
                case JSON_V:
                    result.add(de.getKv().getKey(), JSON_PARSER.parse(de.getKv().getJsonV()));
                    break;
                default:
                    throw new IllegalArgumentException("Unsupported data type: " + de.getKv().getType());
            }
        };
    }

    private static ClaimDeviceMsg buildClaimDeviceMsg(Long deviceId, String secretKey, long durationMs) {
        ClaimDeviceMsg.Builder result = ClaimDeviceMsg.newBuilder();
        return result
                .setDeviceId(deviceId)
                .setSecretKey(secretKey)
                .setDurationMs(durationMs)
                .build();
    }

    private static JsonObject toJson(ProvisionDeviceResponseMsg payload, int requestId) {
        return toJson(payload, true, 0);
    }

    private static JsonObject toJson(ProvisionDeviceResponseMsg payload, boolean toGateway, int requestId) {

        JsonObject result = new JsonObject();
        if (payload.getStatus() == ResponseStatus.NOT_FOUND) {
            result.addProperty("status", ResponseStatus.NOT_FOUND.name());
            result.addProperty("errMsg", "Provision data was not found!");
        } else if (payload.getStatus() == ResponseStatus.FAILURE) {
            result.addProperty("status", ResponseStatus.FAILURE.name());
            result.addProperty("errMsg", "Failed to provision device!");
        } else {
            if (toGateway) {
                result.addProperty("id", requestId);
            }
            switch (payload.getCredentialsType()) {
                case ACCESS_TOKEN:
                case X509_CERTIFICATE:
                    result.addProperty("credentialsValue", payload.getCredentialsValue());
                    break;
                case MQTT_BASIC:
                    JsonObject value = JSON_PARSER.parse(payload.getCredentialsValue()).getAsJsonObject();
                    result.add("credentialsValue", value);
                    break;
                case LWM2M_CREDENTIALS:
                    break;
            }
            result.addProperty("credentialsType", payload.getCredentialsType().name());
            result.addProperty("status", ResponseStatus.SUCCESS.name());
        }
        return result;
    }

    private static void convertToTelemetry(JsonElement jsonElement, long systemTs, PostTelemetryMsg.Builder builder) {

        if (jsonElement.isJsonObject()) {
            parseObject(systemTs, builder, jsonElement.getAsJsonObject());
        } else if (jsonElement.isJsonArray()) {
            jsonElement.getAsJsonArray().forEach(je -> {
                if (je.isJsonObject()) {
                    parseObject(systemTs, builder, je.getAsJsonObject());
                } else {
                    throw new JsonSyntaxException(CAN_T_PARSE_VALUE + je);
                }
            });
        } else {
            throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonElement);
        }
    }

    private static void parseObject(long systemTs, PostTelemetryMsg.Builder target, JsonObject jo) {

        if (jo.has("ts") && jo.has("values")) {
            TsKeyValueListProto.Builder builder = TsKeyValueListProto.newBuilder();
            builder.setTs(systemTs);
            builder.addAllKv(parseValues(jo.get("values").getAsJsonObject()));
            target.addTsKvList(builder.build());
        } else {
            TsKeyValueListProto.Builder builder = TsKeyValueListProto.newBuilder();
            builder.setTs(jo.get("ts").getAsLong());
            builder.addAllKv(parseValues(jo.get("values").getAsJsonObject()));
            target.addTsKvList(builder.build());
        }
    }

    private static List<KeyValueProto> parseValues(JsonObject valuesObject) {

        List<KeyValueProto> result = new ArrayList<>();
        for (Map.Entry<String, JsonElement> valueElement : valuesObject.entrySet()) {
            result.add(parseValue(valueElement));
        }
        return result;
    }

    private static KeyValueProto parseValue(Map.Entry<String, JsonElement> valueEntry) {

        String key = valueEntry.getKey();
        JsonElement element = valueEntry.getValue();
        if (element.isJsonPrimitive()) {
            JsonPrimitive value = element.getAsJsonPrimitive();
            if (value.isString()) {
                return KeyValueProto.newBuilder().setKey(valueEntry.getKey()).setType(KeyValueType.STRING_V)
                        .setStringV(value.getAsString()).build();
            } else if (value.isBoolean()) {
                return KeyValueProto.newBuilder().setKey(valueEntry.getKey()).setType(KeyValueType.BOOLEAN_V)
                        .setBoolV(value.getAsBoolean()).build();
            } else if (value.isNumber()) {
                return buildNumericKeyValueProto(key, value);
            } else {
                throw new JsonSyntaxException(CAN_T_PARSE_VALUE + value);
            }
        } else if (element.isJsonObject() || element.isJsonArray()) {
            return KeyValueProto.newBuilder().setKey(valueEntry.getKey()).setType(KeyValueType.JSON_V)
                    .setJsonV(element.toString()).build();
        } else {
            throw new JsonSyntaxException(CAN_T_PARSE_VALUE + element);
        }
    }

    private static KeyValueProto buildNumericKeyValueProto(String key, JsonPrimitive value) {

        String strValue = value.getAsString();
        KeyValueProto.Builder builder = KeyValueProto.newBuilder().setKey(key);
        BigDecimal deciValue = new BigDecimal(strValue);
        if (deciValue.stripTrailingZeros().scale() <= 0 && !isSimpleDouble(strValue)) {
            return builder.setType(KeyValueType.LONG_V).setLongV(deciValue.longValueExact()).build();
        }
        if (deciValue.scale() <= 16) {
            return builder.setType(KeyValueType.DOUBLE_V).setDoubleV(deciValue.doubleValue()).build();
        }
        throw new JsonSyntaxException("Big integer values are not supported!");
    }

    private static boolean isSimpleDouble(String valueAsString) {
        return valueAsString.contains(".") && !valueAsString.contains("E") && !valueAsString.contains("e");
    }

    private static String getStrValue(JsonObject jo, String field, boolean requiredField) {
        if (jo.has(field)) {
            return jo.get(field).getAsString();
        } else {
            if (requiredField) {
                throw new RuntimeException("Failed to find the field " + field + " in JSON body " + jo + "!");
            }
            return "";
        }
    }
}
